2W - EKS VPC CNI 분석

개요

본 글은 다음의 내용을 설명하고, 이를 위한 실습을 진행한다.

이 노트에서는 EKS의 가장 기본이 되는 네트워크 환경과 리소스를 분석한다.

사전 지식

VPC CNI란

VPC CNIEKS를 위해 제공하는 CNI이다.

Container Network Interface

CNI는 쿠버네티스에서 파드 간의 통신을 위한 요구사항에 맞춰 만들어진 인터페이스를 말한다.
다양한 환경의 클러스터에서 파드 간 통신을 할 수 있도록 쿠버네티스는 이것을 직접 구현하지 않고 각 단체나 개인이 이 인터페이스에 맞춰 통신 방식을 구현하도록 인터페이스를 열어두었다.
이것에 맞춰 다양한 CNI가 개발되고 있으며, 대표적으로는 Cilium, Flannel, Calico 등이 있다.

AWS 클라우드 환경에 최적화되어 있는 네트워크 플러그인인데, 아주 큰 특징이 있다.
바로 VPC의 IP 대역을 파드에게 할당한다는 것..
얼핏 들으면 IP 대역을 빠르게 소모시키는 IP 먹는 하마 같지만(하마는 맞는 것 같다), 그만큼 가지는 이점이 있다.

IP를 할당하는 방식은 인스턴스 ENI의 보조 IP를 주는 방식이다.
그런데 ENI는 스펙마다 최대로 받을 수 있는 IP 개수가 정해져 있다.
그래서 이를 초과하는 만큼 파드가 생성되는 경우에는 추가적인 ENI가 붙게 된다.
그런데 인스턴스는 스펙마다 최대로 받을 수 있는 ENI 개수도 정해져 있다!
그래서 사실.. 한 인스턴스에는 최대로 생성할 수 있는 파드의 개수가 제한된다..
물론 이걸 설정하는 방법이 존재하는데, 아래에서 보자.

장점

구조 및 동작과정


구체적으로 VPC CNI라고 하면 두 가지 구성 요소가 포함된다.
image.png
kube-system 네임스페이스에 데몬셋으로 확인할 수 있는데, 그래서 이 친구는 컨테이너도 두 개다.


두 요소는 서로 gRPC로 통신하며 각종 작업을 수행하게 된다.[1]
CNI 바이너리가 실제로 IP를 할당하고, 그 IP 자체는 IPAM에서 받아온다고 이해하면 딱 맞겠다.


IPAM은 어떻게 IP를 확보할까?
위에서 대충 말했지만, 일단 IP를 받을 공간이 있는지 보고, 없으면 ENI를 하나 더 받아오는 식이다.

최대 IP 개수

aws ec2 describe-instance-types \
    --filters "Name=instance-type,Values=c5.*" \
    --query "InstanceTypes[].{ \
        Type: InstanceType, \
        MaxENI: NetworkInfo.MaximumNetworkInterfaces, \
        IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
    --output table

이런 식으로 인스턴스 스펙 당 최대로 붙일 수 있는 ENI개수와, 그 ENI가 받을 수 있는 최대 IP 개수를 볼 수 있다.
image.png
그럼 내가 사용할 수 있는 ip는 총 몇 개일까?
맨 위 c5.4xlarge를 예로 들어보겠다.
일단 인스턴스에 ENI가 부착되면 한 IP는 노드 자체에 대해 할당된다.
그렇기에 각 ENI당 파드에 할당할 수 있는 IP 주소 개수는 30 - 1 인 29개이다.
그렇기에 해당 인스턴스에 배치할 수 있는 최대 파드는 29 * 8 인 232개가 된다.

hostNetwork를 사용하는 파드는!

클러스터 운영을 위해 기본으로 돌아가는, kube-proxy와 cni 파드가 hostNetwork로 돌아가고 있기에 한 노드당 최대 파드를 구하려면 사실 마지막에 2를 더해주어야 하는데, 어차피 이건 우리가 운영에 직접 사용할 파드도 아니라 제외한다.

kubectl describe node | grep Allocatable: -A6

클러스터의 정보로서도 파드의 개수가 표시되므로, 이렇게도 볼 수 있다.
image.png
현재 내 인스턴스는 t3.medium으로, 호스트 네트워크 파드까지 포함하여 17개까지 파드를 둘 수 있다고 나온다.

설정 방법

image.png
이건 콘솔에서 커스텀 세팅을 한 모습이다.
기본적으로 설정 파일에 대한 스키마를 보고 그것에 맞게 넣어주면 된다.

 aws eks describe-addon-configuration --addon-name vpc-cni --addon-version v1.15.1-eksbuild.1 | jq '.configurationSchema | fromjson'

이런 식으로 스키마를 볼 수 있고, 콘솔에서도 찾아볼 수 있다.

IP 개수 늘리기

VPC CNI의 단점은 IP가 빠르게 소진된다는 것이다.
이를 해결하기 위한 여러가지 방법이 있다.

custom networking

먼저 이 방식은 인스턴스에 배치할 수 있는 파드 개수를 늘리는 것은 아니고, vpc내의 ip 대역을 늘리는 설정이라고 봐야한다.

이런 식으로, VPC 내에 완전히 새로운 서브넷을 지정해서 이 IP를 할당해주는 방법이 존재한다![2]
이걸 활용하는 게 바로 커스텀 네트워킹으로, 새로운 IP 대역을 받아 할당할 수 있게 된다.
10.42 대역의 VPC에서 100.64 대역을 배치하는 괴상한 방식이 가능한 것이다!

prefix delegation

작은 일만 하는 파드들인데 이 제한으로 인해 노드를 여러 개 쓰는 것은 큰 비효율이다.
그래서 한 인스턴스에서 실행할 파드를 늘리기 위해 AWS에서는 접두사 위임(prefix delegation)기능을 제공한다[3]

기존에 각 ENI에서 하나의 파드에 하나씩 할당했던 Secondary IP들이, 전부 하나의 작은 서브넷처럼 작동한다.
원래 그냥 ip를 주던 것으로 끝내지 않고 활용할 수 있는 서브넷 대역을, 즉 접두사를 활용할 수 있게 준다고 하여 접두사 위임이라 부른다.
이렇게 하면 ..../28의 서브넷을 내부적으로 위임받아 파드에 할당할 수 있게 된다.
인스턴스가 받을 수 있는 각 IP들이 내부의 작은 서브넷이 되어 각각 16개의 추가 IP를 할당할 수 있도록 해주는 것이다!
image.png
그냥 보면 헷갈릴 수 있겠지만, 각 ip 대역은 4번째 옥텟의 상위 4비트가 전부 달라서 문제가 발생하지 않는다.

image.png
위 사진에서도 보이듯이, 이게 설정되면 각 노드는 미리 접두사를 할당 받게 된다.

참고로 이렇게 한 노드에 배치할 수 있는 파드의 개수를 늘릴 수는 있지만, 이것과 별개로 kubelet 단에서 제한하는 최대 파드 개수가 있다.
그렇기 때문에 이 기능을 적용하려면 최소한 kubelet을 재기동시켜줘야 한다.
image.png
또한 NItro 하이퍼바이저를 사용한 인스턴스에 대해서만 이 기능을 사용할 수 있다.

실습 진행

환경 세팅에 대한 부분은 2W - 테라폼으로 환경 구성 및 VPC 연결에 담겨져있다!

대충 모양은 이렇게 구성돼있어서 운영 환경의 VPC와 EKS VPC를 분리한 상태이다.

간단하게 VPC CNI 로그 확인

tail -f /var/log/aws-routed-eni/ipamd.log

각 노드로 들어가면 주소를 어떻게 관리하고 있는지 로그를 확인할 수 있다.
image.png
일단 현재 붙은 인터페이스를 추적하고 여기에 미리 선제적으로 ip들을 받아둔다.
image.png
그리고 파드가 생성될 때, 풀에 확보된 ip를 전달해준다.

custom networking 세팅

image.png
먼저 VPC에 추가적인 IP cidr을 설정하는 것으로 시작한다.
image.png
VPC cidr 대역을 추가 할당했다면 거기에 상응하는 서브넷도 만들어주면 기본 준비는 끝났고, 이제 CNI 세팅을 건드리자.

kubectl set env daemonset aws-node -n kube-system AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true

vpc cni 쪽에서는 데몬셋 환경 설정을 통해 활성화할 수 있다.
환경 변수의 변경도 컨테이너의 변경으로 치기 때문에 알아서 워크로드는 재시작된다.

apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata:
  # 가용영역 이름을 웬만해서 넣어야 한다.나도 알고 싶지 않았다.
  name: ${SUBNET_AZ}
spec:
  # 허용하고자 하는 보안그룹 id
  securityGroups:
    - ${EKS_CLUSTER_SECURITY_GROUP_ID}
  # 만들어둔 서브넷 id
  subnet: ${SECONDARY_SUBNET}

여기에 ENIConfig라는 CRD를 만들어주면 진짜 준비는 끝이다.
어떤 AZ에 어떤 추가 서브넷이 있는지 명시하는 방식이다.
특이한 점은 가용영역을 이름으로 넣는다는 것.

만약 하나의 가용영역에 여러 개의 서브넷이 있는 상황이라면 당연히 이름 충돌이 발생한다.
image.png
이 경우에는, 이름은 알아서 만든 후에 각 노드 별로 k8s.amazonaws.com/eniConfig=EniConfigName 이런 식으로 어노테이션을 붙여줘야 한다.[4]

테스트

image.png
나는 직접적으로 하나를 넣어보았다.

kubectl set env daemonset aws-node -n kube-system ENI_CONFIG_LABEL_DEF=topology.kubernetes.io/zone

이제 CNI에서 해당 CONFIG 파일을 인식할 수 있도록 재동작을 시켜주면 된다.
image.png
이 다음에는? 해당 서브넷을 쓰는 새로운 노드 그룹을 만들면 된다!
간단하게 콘솔로만 진행했다.
image.png
파드를 해당 노드에 투입해봤다.
VPC의 IP가 부족하다면 이런 식으로 추가 대역을 넣어서 올리는 방법이 유용할 것이다.
다만, 이 경우 추가적인 노드를 추가시켜야 한다는 것이 마음에 걸린다.

prefix delegation

kubectl set env daemonset aws-node -n kube-system ENABLE_PREFIX_DELEGATION=true

이것도 설정 자체는 이렇게 CNI에 환경 설정을 넣는 것만으로 가능하다.
image.png
콘솔로 확인해보면 기존에 붙어있던 eni에 prefix delegation이 붙는 걸 볼 수 있다.
노드에 기본적으로 세팅할 수 있는 파드 개수의 최대값이 이미 지정되어 있기에 인스턴스를 재기동해야 할 수도 있다.
파드 최대 개수 설정이 재설정이 돼야 하기 때문이다.
이때 커스텀 런치 템플릿을 사용한 경우, 거기에서 파드 최대 개수를 명시적으로 바꿔줘야 한다.
image.png
런치 템플릿이 따로 없던 노드 그룹만 할당 파드 개수가 제대로 적용됐다.
image.png
이제 한 노드에 파드 폭격을 가해도 제대로 파드들이 여러 개 만들어진다!

통신 경로 분석

이제 VPC CNI로 구성된 클러스터의 네트워크 경로를 간단하게 분석해보고자 한다.
image.png
대충 예시 파드를 하나 만들어보고 확인해보자.
여기에서 192.168.1.112 파드는 어떻게 통신이 될 수 있을까?
image.png
해당 노드에 들어가서 ip addr, ip route를 했을 때의 값이다.
112 ip로 가는 통신은 eni77~로 가도록 되어 있다.
그럼 이 인터페이스는 또 어디로 이어지는지 확인해보자.

lsns -t net

lsns를 통해 현 호스트이 네임스페이스를 확인할 수 있다.
이때 type을 지정해서 네트워크 네임스페이스를 확인해보면..
image.png
현재 띄워져있는 파드의 pause container 프로세스가 보인다!

export PID=$(lsns -t net | grep pause | awk '{print $4}')
nsenter -t $PID --net ip addr
nsenter -t $PID --net ip route

프로세스 id까지 알았으니, 이제 현 터미널을 해당 네임스페이스를 사용하게 만들 수 있다.

ip link
nsenter -t $PID --net ip link

그럼 이제 어떤 인터페이스가 서로 연결됐는지도 확인할 수 있다.
image.png
루트 네임스페이스에서 eni22bc~는 link-netns로 cni-23~로 연결된다는 것을 알 수 있으며, 파드의 네임스페이스에선 id 0으로 연결이 되는 것이 확인된다.

ip netns list

이렇게 ip 명령어를 통해 해서 보는 방법이 있다(물론 위의 lsns로도 확인 가능하다).
image.png
이렇게 컨테이너로 어떻게 통신이 이뤄지는지 비로소 알 수 있다.

실제 통신 확인

이번에는 실제로 패킷이 어떻게 전달되는지 직접적으로 확인해보자.

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
  labels:
    app: target
spec:
  containers:
  - image: nginx
    name: nginx
    ports:
    - containerPort: 80
  nodeName: ip-192-168-1-61.ap-northeast-2.compute.internal
---
apiVersion: v1
kind: Pod
metadata:
  name: inside-node
spec:
  containers:
  - image: nicolaka/netshoot
    name: test
    command:
    - sh
    - -c
    - "sleep infinity"
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - key: app
                operator: In
                values:
                  - target
          topologyKey: "kubernetes.io/hostname"
---
apiVersion: v1
kind: Pod
metadata:
  name: inter-node
spec:
  containers:
  - image: nicolaka/netshoot
    name: test
    command:
    - sh
    - -c
    - "sleep infinity"
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - key: app
                operator: In
                values:
                  - target
          topologyKey: "kubernetes.io/hostname"

테스트용 nginx를 띄운 뒤, Affinity 세팅을 통해 한 파드는 같은 노드에, 다른 하나는 다른 노드에 띄워지도록 만들었다.
image.png
각각의 파드의 ip는 이렇게 할당됐다.

k exec -ti inside-node -- zsh

이런 식으로 각 파드에 들어가서 통신을 해볼 것이다.

tcpdump -nn -i any dst port 80

이런 식으로 패킷을 추적한다.

노드 내부 통신

테스트 파드가 있는 노드에서 먼저 추적해본다.

tcpdump -nn -i any dst 192.168.1.96

test-pod는 96으로 끝나는 ip를 가지고 있으니 파드로 가는 요청을 한번 보자.
image.png
97로 끝나는 inside-node 파드에서 요청이 날아갔다.
image.png
참고로 eni 인터페이스는 inside-node, test-pod에게 붙은 네트워크 인터페이스이다.
서로가 서로에게 바로바로 통신을 주고받는 것이 확인된다.
image.png
사진으로 싣지는 않겠지만, inside node의 파드의 eth0에서 트래픽이 나갈 때는 97로 나가는데, 이후 노드의 라우팅 테이블에서 96이라는 ip에 대해 eni7fba~로 가도록 되어 있었기 때문에 자연스럽게 별다른 트래픽 이동없이 바로 연결이 된 것이다.

노드 간 통신

k exec -ti inter-node -- zsh

이번에는 다른 노드에서 요청을 보낼 것이다.
image.png
이번에도 192.1682.96, 즉 해당 파드의 ip를 달고 통신이 이뤄진 것을 확인할 수 있다.
그러나 이것은 얼핏 보면 조금 이상하다.
image.png
노드를 나가는 트래픽이 다시 돌아오기 위해서는 통상적으로 노드의 IP를 소스로 가지는 식으로, 즉 SNAT를 해서 나가야만 제대로 돌아올 수 있다.
그래서 흔히 노드의 ip가 추적됐어야 할 것만 같다.
그런데 위의 결과는 소스가 파드의 ip를 그대로 가지고 있는 모습이다.

iptables --list -t nat -n | grep  AWS -A1 -B3

해당 답의 힌트는 iptables에서 얻을 수 있다.
image.png
트래픽이 나가기 직전에 적용되는 POSTROUTING 체인을 보면 쿠버 클러스터에 흔히 세팅되는 KUBE-POSTROUTING외에 AWS-SNAT-CHAIN-0을 볼 수 있다.
그리고 해당 체인은 192.168.0.0/16에 대해서 RETURN을 하므로, 그대로 POSTROUTING으로 돌아가게 되고 결국 SNAT 없이 트래픽은 인터페이스를 타고 나가게 된다.

ip route get 192.168.1.96

참고로 해당 경로를 나갈 때는..
image.png
이렇게 ens5라는, 인스턴스 바깥으로 나가는 경로를 탄다.
SRC 힌트가 붙어 있어서 무조건 SNAT이 될 거라고 생각했지만, 막상 규칙으로는 SNAT 없이 게이트웨이로 가게 되는 것이다.
그래서 결과적으로 VPC CNI는 클러스터 내부에서 모두 자신의 IP를 달고 통신을 하게 된다.

watch -d 'sudo iptables -v --numeric --table nat --list AWS-SNAT-CHAIN-0; echo ; sudo iptables -v --numeric --table nat --list KUBE-POSTROUTING; echo ; sudo iptables -v --numeric --table nat --list POSTROUTING'

참고로 이런 식으로 watch를 해두면 패킷이 나가는 개수도 확인할 수 있다.

노드 외부로 통신

ping google.com

마지막으로 외부로 통신이 나가는 상황을 본다.
테스트는 test-pod가 있는 위치에서 진행했다.

tcpdump -nn -i any icmp

image.png
이번에는 파드에서 나가는 요청이 SNAT되어 ens5 인터페이스를 탈 때는 노드의 IP가 찍히는 것을 확인할 수 있다.
위의 iptables(위 그림은 다른 노드에서 찍은 거지만 아무튼..)에서 RETURN되지 않고 SNAT 규칙에 걸렸기 때문이다.

SNAT 비활성화

당연히 SNAT되는 게 정상적인 것 같지만, 이를 비활성화하는 것도 가능하다.
만약 VPC 피어링을 하고 있는 경우라면 어차피 서로의 대역에 대해 경로를 알고 있으므로 비활성되더라도 통신에 문제는 없을 것이다.
image.png
먼저 확인을 위해 운영 호스트 보안 그룹에 eks 클러스터 보안그룹에서의 ping을 허용하도록 세팅했다.

k exec -ti inside-node -- ping 172.20.0.25

image.png
파드에서 운영 호스트로 ping을 날리니 워커 노드의 ip로 값이 들어오는 것이 확인된다.

kubectl set env daemonset aws-node -n kube-system AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS=172.20.0.0/16

CNI에 이렇게 환경 변수를 설정해주면, SNAT를 하지 않을 대역을 지정할 수 있다.
image.png
다시 ping을 날리면, 이번에는 파드의 IP가 그대로 찍히는 것이 확인된다!
참고로 1.8 버전 이전의 VPC CNI는 secondary 네트워크 인터페이스에서 ip를 할당 받은 파드의 경우에 대해 들어오고 나가는 인터페이스 경로가 달라서 rp_filter에 의해 패킷이 드랍되는 경우가 있었다고 한다.[5]

결론

VPC CNI는 파드가 노드와 같은 IP를 가지게 하는 특이한 세팅을 통해, 추가적인 패킷 캡슐화 없이 통신이 가능하게 해준다.
그만큼 빠르게 IP가 소모될 여지가 있지만 이를 다양한 세팅을 통해 해결할 수 있다.

다음 글에서 다룰 텐데, 이렇게 VPC의 IP를 받음으로써 다른 네트워크 리소스들에서 할 수 있는 기능들이 생기기 때문에 웬만하면 EKS를 쓸 때는 VPC CNI를 쓰는 것이 강권된다.

이전 글, 다음 글

다른 글 보기

이름 index noteType created
1W - EKS 설치 및 액세스 엔드포인트 변경 실습 1 published 2025-02-03
2W - 테라폼으로 환경 구성 및 VPC 연결 2 published 2025-02-11
2W - EKS VPC CNI 분석 3 published 2025-02-11
2W - ALB Controller, External DNS 4 published 2025-02-15
3W - kubestr과 EBS CSI 드라이버 5 published 2025-02-21
3W - EFS 드라이버, 인스턴스 스토어 활용 6 published 2025-02-22
4W - 번외 AL2023 노드 초기화 커스텀 7 published 2025-02-25
4W - EKS 모니터링과 관측 가능성 8 published 2025-02-28
4W - 프로메테우스 스택을 통한 EKS 모니터링 9 published 2025-02-28
5W - HPA, KEDA를 활용한 파드 오토스케일링 10 published 2025-03-07
5W - Karpenter를 활용한 클러스터 오토스케일링 11 published 2025-03-07
6W - PKI 구조, CSR 리소스를 통한 api 서버 조회 12 published 2025-03-15
6W - api 구조와 보안 1 - 인증 13 published 2025-03-15
6W - api 보안 2 - 인가, 어드미션 제어 14 published 2025-03-16
6W - EKS 파드에서 AWS 리소스 접근 제어 15 published 2025-03-16
6W - EKS api 서버 접근 보안 16 published 2025-03-16
7W - 쿠버네티스의 스케줄링, 커스텀 스케줄러 설정 17 published 2025-03-22
7W - EKS Fargate 18 published 2025-03-22
7W - EKS Automode 19 published 2025-03-22
8W - 아르고 워크플로우 20 published 2025-03-30
8W - 아르고 롤아웃 21 published 2025-03-30
8W - 아르고 CD 22 published 2025-03-30
8W - CICD 23 published 2025-03-30
9W - EKS 업그레이드 24 published 2025-04-02
10W - Vault를 활용한 CICD 보안 25 published 2025-04-16
11W - EKS에서 FSx, Inferentia 활용하기 26 published 2025-04-18
11주차 - EKS에서 FSx, Inferentia 활용하기 26 published 2025-05-11
12W - VPC Lattice 기반 gateway api 27 published 2025-04-27

관련 문서

이름 noteType created
VPC CNI knowledge 2025-02-11

참고


  1. https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md ↩︎

  2. https://www.eksworkshop.com/docs/networking/vpc-cni/custom-networking/ ↩︎

  3. https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/cni-increase-ip-addresses.html ↩︎

  4. https://github.com/aws/amazon-vpc-cni-k8s ↩︎

  5. https://blog.naver.com/alice_k106/222457199073 ↩︎